JavaScript 56.7%
TypeScript 42.6%
1import { NextRequest, NextResponse } from "next/server";2import JSZip from "jszip";3import { prisma } from "@/lib/prisma";4import { loadFileData } from "@/lib/storage";5import { fileInShare, folderInShare, hasPasswordAccess, loadShare, shareInfo, shareStatusError, touchShare, type ShareWithTargets } from "@/lib/shareAccess";67function fileResponse(buffer: Buffer, name: string, mimeType: string, download: boolean) {8 return new NextResponse(new Uint8Array(buffer), {9 headers: {10 "Content-Type": download ? "application/octet-stream" : mimeType,11 "Content-Length": String(buffer.length),12 "Content-Disposition": `${download ? "attachment" : "inline"}; filename*=UTF-8''${encodeURIComponent(name)}`,13 "Cache-Control": "private, no-store",14 },15 });16}1718async function addFolderToZip(zip: JSZip, folderId: string, prefix: string) {19 const files = await prisma.file.findMany({ where: { folderId, deletedAt: null } });20 for (const file of files) {21 try {22 zip.file(prefix + file.name, await loadFileData(file.storagePath, file.isEncrypted));23 } catch { /* fichier illisible : ignoré */ }24 }25 const subfolders = await prisma.folder.findMany({ where: { parentId: folderId } });26 for (const sub of subfolders) await addFolderToZip(zip, sub.id, `${prefix}${sub.name}/`);27}2829/** Listing d'un dossier du partage : fichiers actifs, sous-dossiers, fil d'Ariane borné à la racine partagée. */30async function folderListing(share: ShareWithTargets, folderId: string) {31 const folder = await prisma.folder.findUnique({ where: { id: folderId }, select: { id: true, name: true, parentId: true } });32 if (!folder) return null;33 const [files, children] = await Promise.all([34 prisma.file.findMany({ where: { folderId, deletedAt: null }, orderBy: { name: "asc" }, select: { id: true, name: true, mimeType: true, size: true, updatedAt: true } }),35 prisma.folder.findMany({ where: { parentId: folderId }, orderBy: { name: "asc" }, select: { id: true, name: true, _count: { select: { files: true, children: true } } } }),36 ]);37 const crumbs: { id: string; name: string }[] = [];38 let cur: { id: string; name: string; parentId: string | null } | null = folder;39 while (cur) {40 crumbs.unshift({ id: cur.id, name: cur.name });41 if (cur.id === share.folder!.id) break;42 cur = cur.parentId ? await prisma.folder.findUnique({ where: { id: cur.parentId }, select: { id: true, name: true, parentId: true } }) : null;43 }44 return {45 folder: { id: folder.id, name: folder.name },46 crumbs,47 folders: children.map((c) => ({ id: c.id, name: c.name, files: c._count.files, children: c._count.children })),48 files: files.map((f) => ({ ...f, size: Number(f.size) })),49 };50}5152export async function GET(53 request: NextRequest,54 { params }: { params: { token: string } }55) {56 const share = await loadShare(params.token);57 const err = shareStatusError(share);58 if (err || !share) return err!;5960 const { searchParams } = new URL(request.url);61 const action = searchParams.get("action");62 const wantContent = searchParams.get("content") === "true";63 const fileId = searchParams.get("fileId");64 const folderId = searchParams.get("folderId");6566 // Mot de passe non validé (cookie signé absent) : on ne révèle que le minimum67 if (!hasPasswordAccess(request, share)) {68 const info = shareInfo(share);69 return NextResponse.json({70 requiresPassword: true,71 type: info.type,72 name: info.name,73 fileName: info.name, // compatibilité ancien client74 fileType: share.file?.mimeType ?? "inode/directory",75 mode: share.mode,76 expiresAt: info.expiresAt,77 });78 }7980 // ---- Partage de FICHIER ----81 if (share.file) {82 if (wantContent || action === "download") {83 const buffer = await loadFileData(share.file.storagePath, share.file.isEncrypted);84 const download = action === "download" || share.mode === "DOWNLOAD";85 await touchShare(share.id);86 return fileResponse(buffer, share.file.name, share.file.mimeType, download);87 }88 await touchShare(share.id);89 return NextResponse.json(shareInfo(share));90 }9192 // ---- Partage de DOSSIER ----93 const root = share.folder!;9495 if (fileId) {96 const f = await fileInShare(share, fileId);97 if (!f) return NextResponse.json({ error: "Fichier hors du partage" }, { status: 404 });98 if (wantContent || action === "download") {99 const buffer = await loadFileData(f.storagePath, f.isEncrypted);100 await touchShare(share.id);101 return fileResponse(buffer, f.name, f.mimeType, action === "download" || share.mode === "DOWNLOAD");102 }103 return NextResponse.json({ id: f.id, name: f.name, mimeType: f.mimeType, size: Number(f.size), mode: share.mode });104 }105106 // ZIP du dossier partagé (ou d'un sous-dossier)107 if (action === "download") {108 const target = folderId ?? root.id;109 if (!(await folderInShare(share, target))) return NextResponse.json({ error: "Dossier hors du partage" }, { status: 404 });110 const folder = await prisma.folder.findUnique({ where: { id: target }, select: { name: true } });111 const zip = new JSZip();112 await addFolderToZip(zip, target, "");113 const buffer = await zip.generateAsync({ type: "nodebuffer", compression: "DEFLATE", compressionOptions: { level: 6 } });114 await touchShare(share.id);115 return new NextResponse(new Uint8Array(buffer), {116 headers: {117 "Content-Type": "application/zip",118 "Content-Disposition": `attachment; filename*=UTF-8''${encodeURIComponent((folder?.name || root.name) + ".zip")}`,119 "Cache-Control": "private, no-store",120 },121 });122 }123124 // Listing (racine du partage ou sous-dossier)125 const target = folderId ?? root.id;126 if (!(await folderInShare(share, target))) return NextResponse.json({ error: "Dossier hors du partage" }, { status: 404 });127 const listing = await folderListing(share, target);128 if (!listing) return NextResponse.json({ error: "Dossier introuvable" }, { status: 404 });129 await touchShare(share.id);130 return NextResponse.json({ ...shareInfo(share), ...listing });131}132